feat: add LLM provider pricing info dialog - #3484
Conversation
Enable separating non-secret LLM provider config (model, prices, type) from secrets (API keys) in Kubernetes deployments via a new `provider-defaults` map that merges with the existing `providers` list.
Add a pricing information dialog for server-configured LLM providers. Organization owners can now see token pricing in credits and estimated costs for translating 1,000 strings. - Extend LlmProviderSimpleModel with tokenPriceInCreditsInput/Output - Create LlmProviderPricingDialog with token prices table, cost estimates (with/without screenshots), and info notes - Create LlmProviderPricingInfo as self-contained icon + dialog - Only show pricing info when billing is enabled - Fetch subscription data to convert credits to USD - Add E2E test verifying pricing info is hidden without billing
📝 WalkthroughWalkthroughThis PR adds token pricing support for LLM providers. It introduces provider-level token price defaults in configuration, implements merge logic in the service layer to combine defaults with explicit provider configurations, exposes pricing data through API models, and adds a React UI component that conditionally displays pricing information when billing is enabled. Changes
Sequence Diagram(s)sequenceDiagram
participant Config as Config Layer
participant Service as LlmPropertiesService
participant API as API Assembler
participant Frontend as React UI
Config->>Service: providerDefaults + providers list
Service->>Service: getMergedProviders()
Note over Service: Merge map defaults with list entries<br/>Apply overrides & defaults
Service->>API: Merged LlmProvider objects
API->>API: Map to LlmProviderSimpleModel<br/>Include token prices
API->>Frontend: JSON with pricing data
Frontend->>Frontend: Check billing.enabled
alt Billing Enabled
Frontend->>Frontend: Render LlmProviderPricingInfo
Note over Frontend: Display pricing button & dialog
else Billing Disabled
Frontend->>Frontend: Skip pricing component
Note over Frontend: No pricing info shown
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@e2e/cypress/e2e/llmProviders/llmProviders.cy.ts`:
- Around line 103-107: The test "does not show pricing info when billing is
disabled" currently uses text-based selection
gcy('organization-llm-providers-tab').contains('Server').click(); change it to
target a dedicated data-cy for the server tab (e.g.
gcy('organization-llm-providers-server-tab').click()) and update the
DOM/component to include that data-cy if missing; keep the rest of the
assertions (gcy('llm-provider-item-name') and gcy('llm-provider-pricing-info'))
unchanged so the test uses only data-cy selectors per guidelines.
In `@webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx`:
- Around line 38-44: The IconButton that opens the pricing info modal is
icon-only and lacks an accessible name; update the IconButton (the component
rendering InfoCircle and calling setOpen) to include an aria-label prop using
the existing translation key used for this tooltip/label in this module (i.e.,
add aria-label={t('...existingKey...')} or the specific translation constant
already present in this file) so screen readers get a proper accessible name.
- Around line 1-5: The relative import of LlmProviderPricingDialog should use
the project's tg.* alias; update the import statement that currently references
'./LlmProviderPricingDialog' in LlmProviderPricingInfo (the file importing
LlmProviderPricingDialog) to use
'tg.ee.module/llm/OrganizationLLMProviders/LlmProviderPricingDialog' so the
module resolver uses the Tolgee path alias.
| it('does not show pricing info when billing is disabled', () => { | ||
| gcy('organization-llm-providers-tab').contains('Server').click(); | ||
| gcy('llm-provider-item-name').should('contain', 'server-provider'); | ||
| gcy('llm-provider-pricing-info').should('not.exist'); | ||
| }); |
There was a problem hiding this comment.
Avoid text-based selection for the “Server” tab.
Use a dedicated data-cy on the server tab and target it via gcy(...), instead of .contains('Server').
🛠️ Proposed adjustment
- gcy('organization-llm-providers-tab').contains('Server').click();
+ gcy('organization-llm-providers-tab-server').click();As per coding guidelines: **/*.{tsx,ts}: STRICTLY use data-cy attributes for E2E selectors, never rely on text content; use typed helpers gcy() or cy.gcy().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@e2e/cypress/e2e/llmProviders/llmProviders.cy.ts` around lines 103 - 107, The
test "does not show pricing info when billing is disabled" currently uses
text-based selection
gcy('organization-llm-providers-tab').contains('Server').click(); change it to
target a dedicated data-cy for the server tab (e.g.
gcy('organization-llm-providers-server-tab').click()) and update the
DOM/component to include that data-cy if missing; keep the rest of the
assertions (gcy('llm-provider-item-name') and gcy('llm-provider-pricing-info'))
unchanged so the test uses only data-cy selectors per guidelines.
| import { Box, IconButton, styled, Tooltip } from '@mui/material'; | ||
| import { useState } from 'react'; | ||
| import { InfoCircle } from '@untitled-ui/icons-react'; | ||
| import { useTranslate } from '@tolgee/react'; | ||
| import { LlmProviderPricingDialog } from './LlmProviderPricingDialog'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find tsconfig files with path alias configuration
echo "=== Looking for tsconfig files and path alias config ==="
find . -maxdepth 3 -name "tsconfig*.json" -type f | head -20
echo ""
echo "=== Checking webapp tsconfig for tg.* aliases ==="
cat webapp/tsconfig.json 2>/dev/null | jq '.compilerOptions.paths' 2>/dev/null || cat webapp/tsconfig.json 2>/dev/null | grep -A 20 '"paths"'
echo ""
echo "=== Finding existing tg.* import usage in webapp/src/ee/llm ==="
rg 'from\s+["\']tg\.(component|service|hooks|views|globalContext)' webapp/src/ee/llm -A 1 -B 1 || echo "No tg.* imports found in ee/llm"
echo ""
echo "=== Finding all imports in the target file ==="
cat -n webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx | head -10
echo ""
echo "=== Checking what LlmProviderPricingDialog exports ==="
cat -n webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingDialog.tsx | head -15Repository: tolgee/tolgee-platform
Length of output: 717
🏁 Script executed:
#!/bin/bash
echo "=== Direct inspection of webapp/tsconfig.json ==="
cat webapp/tsconfig.json
echo ""
echo "=== Finding tg.* imports in webapp codebase ==="
rg 'from ["\047]tg\.' webapp/src -l --max-count 5
echo ""
echo "=== Actual target file imports ==="
head -10 webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx
echo ""
echo "=== Check if LlmProviderPricingDialog.tsx is a component ==="
head -20 webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingDialog.tsx
echo ""
echo "=== Find other imports in same directory ==="
rg 'from ["\047]\.' webapp/src/ee/llm/OrganizationLLMProviders/ -c | head -20Repository: tolgee/tolgee-platform
Length of output: 2250
🏁 Script executed:
#!/bin/bash
echo "=== Checking tsconfig.extend.json for path aliases ==="
cat webapp/tsconfig.extend.json
echo ""
echo "=== Simple search for any 'tg.component' usage in webapp ==="
rg 'tg\.component' webapp/src --max-count 3
echo ""
echo "=== Simple search for any 'tg.' imports ==="
rg "tg\." webapp/src --max-count 3
echo ""
echo "=== Check vite config for path aliases ==="
fd 'vite.config' webapp/ -x cat {}Repository: tolgee/tolgee-platform
Length of output: 50379
Replace the relative import with a Tolgee path alias.
This import should use the repo's tg.* aliases. Change from './LlmProviderPricingDialog' to from 'tg.ee.module/llm/OrganizationLLMProviders/LlmProviderPricingDialog'.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx` around
lines 1 - 5, The relative import of LlmProviderPricingDialog should use the
project's tg.* alias; update the import statement that currently references
'./LlmProviderPricingDialog' in LlmProviderPricingInfo (the file importing
LlmProviderPricingDialog) to use
'tg.ee.module/llm/OrganizationLLMProviders/LlmProviderPricingDialog' so the
module resolver uses the Tolgee path alias.
| <IconButton | ||
| size="small" | ||
| color="inherit" | ||
| onClick={() => setOpen(true)} | ||
| > | ||
| <InfoCircle /> | ||
| </IconButton> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check if the file exists and read the content around lines 38-44
head -50 webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx | cat -nRepository: tolgee/tolgee-platform
Length of output: 1772
🏁 Script executed:
# Also search for the translation key mentioned in the suggestion
rg "llm_provider_pricing_dialog_title" webapp/Repository: tolgee/tolgee-platform
Length of output: 339
🏁 Script executed:
# Check for other IconButton uses in the codebase to see if aria-label is a common pattern
rg -n "IconButton" webapp/src --type ts --type tsx -C2 | grep -A2 -B2 "aria-label" | head -30Repository: tolgee/tolgee-platform
Length of output: 93
🏁 Script executed:
# Search for IconButton with aria-label to see if it's a pattern
rg "IconButton" webapp/src -A3 -B1 | grep -A3 -B1 "aria-label" | head -40Repository: tolgee/tolgee-platform
Length of output: 1752
🏁 Script executed:
# Also check if there are other icon-only buttons in similar contexts
rg "<InfoCircle" webapp/src -B5 -A5 | head -50Repository: tolgee/tolgee-platform
Length of output: 3903
🏁 Script executed:
# Check guidelines or accessibility patterns in the codebase
fd "accessibility\|a11y\|aria" webapp/src --type fRepository: tolgee/tolgee-platform
Length of output: 48
Add an accessible label to the icon-only button.
Icon-only IconButton needs an accessible name; the tooltip is not a reliable screen-reader label. Add aria-label with the existing translation key.
🛠️ Suggested fix
<IconButton
size="small"
color="inherit"
+ aria-label={t('llm_provider_pricing_dialog_title')}
onClick={() => setOpen(true)}
>
<InfoCircle />
</IconButton>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <IconButton | |
| size="small" | |
| color="inherit" | |
| onClick={() => setOpen(true)} | |
| > | |
| <InfoCircle /> | |
| </IconButton> | |
| <IconButton | |
| size="small" | |
| color="inherit" | |
| aria-label={t('llm_provider_pricing_dialog_title')} | |
| onClick={() => setOpen(true)} | |
| > | |
| <InfoCircle /> | |
| </IconButton> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@webapp/src/ee/llm/OrganizationLLMProviders/LlmProviderPricingInfo.tsx` around
lines 38 - 44, The IconButton that opens the pricing info modal is icon-only and
lacks an accessible name; update the IconButton (the component rendering
InfoCircle and calling setOpen) to include an aria-label prop using the existing
translation key used for this tooltip/label in this module (i.e., add
aria-label={t('...existingKey...')} or the specific translation constant already
present in this file) so screen readers get a proper accessible name.
Summary
LlmProviderSimpleModelwithtokenPriceInCreditsInputandtokenPriceInCreditsOutputfieldsTest plan
perThousandMtCreditsSummary by CodeRabbit
New Features
Tests